﻿/********************************************************************************
* Copyright 2010 Zane Thorn (zane.thorn@gmail.com)                              *
*                                                                               *
* NeturalMath is free software: you can redistribute it and/or modify           *
* it under the terms of the GNU Lesser General Public License as published by   *
* the Free Software Foundation, either version 3 of the License, or             *
* (at your option) any later version.                                           *
*                                                                               *
* NeturalMath is distributed in the hope that it will be useful,                *
* but WITHOUT ANY WARRANTY; without even the implied warranty of                *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                 *
* GNU Lesser General Public License for more details.                           *
*                                                                               *
* You should have received a copy of the GNU Lesser General Public License      *
* along with NeturalMath.  If not, see <http://www.gnu.org/licenses/>.          *
********************************************************************************/

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text;
using NeturalMath.Expressions;
using NeturalMath.Properties;

namespace NeturalMath
{
    public class BlockSymbol:MathSymbol
    {
        #region Events

        public event EventHandler EnteredScope;

        public event EventHandler ExitedScope;

        #endregion

        #region Local Members

        private readonly Dictionary<string, MathSymbol> _symbols = new Dictionary<string, MathSymbol>();
        private Domain _sourceScope;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new instance of a domain.  To only be used internally.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="parentDomain"></param>
        protected internal BlockSymbol(string name,BlockSymbol parentScope,MathRuntime runtime,DomainExpression expression,Scope scope)
            :base(name,parentScope,runtime,expression,scope)
        {
            if (parentScope != null)
            {
                //parentDomain._symbols.Add(Name,this);
                Depth = parentScope.Depth + 1;
            }
        }

        #endregion

        #region Public Properties

        public bool IsCurrent { get; private set; }
        public int Depth { get; private set; }

        #endregion

        #region Public Methods

        #region Block Methods

        public void Enter()
        {
            EnterScope();

            base.Invoke();
        }

        public void Exit()
        {
            if (_sourceScope == null)
                throw new MathException(ErrorCodes.ApplicationAbort, Resources.ApplicationAborted);

            _sourceScope.IsCurrent = true;
            this.IsCurrent = false;

            Runtime.SetCurrentScope(_sourceScope);

            _sourceScope = null;

            if (ExitedScope != null)
                ExitedScope(this, EventArgs.Empty);
        }

        public override MathValue Invoke()
        {
            if (IsCurrent)
                throw new MathException(ErrorCodes.CannotInvokeDomainWithinItself,
                                        Resources.CannotInvokeDomainWithItself);

            EnterScope();

            var result = base.Invoke();

            Exit();

            return result;
        }

        #endregion

        #region Symbol Methods

        public IEnumerable<MathSymbol> LocalSymbols
        {
            get { return Symbols.Values; }
        }

        public IEnumerable<MathSymbol> AllSymbols
        {
            get
            {
                foreach (var s in LocalSymbols)
                    yield return s;

                if (ParentScope != null)
                    foreach (var s in ParentScope.AllSymbols)
                        yield return s;
            }
        }

        public bool HasSymbol(string symbolName)
        {
            return Symbols.ContainsKey(symbolName);
        }

        public MathSymbol GetSymbol(string symbolName)
        {
            // if the symbol has a dot, go to the dotted symbol lookup
            if (symbolName.Contains('.'))
                return GetDotedSymbol(symbolName);

            // check locally first
            if (HasSymbol(symbolName))
                return Symbols[symbolName];

            // if the symbol is not found locally, go lookup from parents
            if (ParentScope != null)
                return ParentScope.GetSymbol(symbolName);

            // the symbol must not exist anywhere, so return null
            return null;
        }

        private MathSymbol GetDotedSymbol(string symbolName)
        {
            // validate that we are performing a legitimate action
            if (!symbolName.Contains('.'))
                throw new MathException(ErrorCodes.InvalidOperation, Resources.InvalidCallToFunction);

            // check to see if we are making an absolute call from the root
            if (symbolName.StartsWith("_"))
            {
                var path = symbolName.Substring(2);
                return Runtime.RootDomain.GetDotedSymbol(path);
            }

            // cut the string into first and last parts
            var dotIndex = symbolName.IndexOf('.');
            var first = symbolName.Substring(0, dotIndex);
            var rest = symbolName.Substring(dotIndex + 1);

            // get the symbol matching the first part of the lookup
            var firstSymbol = GetSymbol(first);

            // if the symbol is null, something is wrong
            if (firstSymbol == null)
                throw new MathException(ErrorCodes.MemberNotFound, string.Format(Resources.MemberNotFound_M, firstSymbol));

            // if the symbol is not a domain, the lookup is invalid
            var domain = firstSymbol as Domain;
            if (domain == null)
                throw new MathException(ErrorCodes.DottedExpressionTargetNotDomain, string.Format(Resources.DottedExpressionTargetNotDomain_T, firstSymbol));

            // continue to lookup based on the child members of the located domain
            var symbol = domain.GetSymbol(rest);
            if (symbol.Scope.Accessability == AccessabilityLevels.Private)
            {
                throw new MathException(ErrorCodes.CannotAccessPrivateOrLocalMember, "Cannot access local or private member");
            }

            return symbol;
        }

        public virtual MathSymbol CloneSymbol(string symbolName, MathSymbol sourceSymbol, Scope scope = default(Scope))
        {
            // trying to assign to a value which does not exist!
            if (HasSymbol(symbolName))
                throw new MathException(ErrorCodes.DuplicateVariable, string.Format(Resources.MemberNameAlreadyExists_M, symbolName));

            // next check to see if symbol is a function
            var lookupFunction = sourceSymbol as Function;
            if (lookupFunction != null)
                return CloneFunction(symbolName, lookupFunction, scope);

            // finally, since it is not a domain or a function the value we are
            // cloning MUST be a variable
            var lookupVariable = sourceSymbol as Variable;
            if (lookupVariable == null)
                throw new MathException(ErrorCodes.InvalidOperation, Resources.UnrecognizedDataType);// something must be wrong in the system

            return CloneVariable(symbolName, lookupVariable, scope);

        }

        #endregion

        #region Function Methods

        public IEnumerable<Function> LocalFunctions
        {
            get { return LocalSymbols.Where(d => d is Function).Select(d => (Function)d); }
        }

        public IEnumerable<Function> AllFunctions
        {
            get { return AllSymbols.Where(d => d is Function).Select(d => (Function)d); }
        }

        public Function GetFunction(string name)
        {
            return GetSymbol(name) as Function;
        }

        public bool HasFunction(string name)
        {
            return HasSymbol(name);
        }

        public Function CreateFunction(string symbolName, ValueExpression expression, IEnumerable<FunctionParameterExpression> parameters, Scope scope = default(Scope))
        {
            if (HasSymbol(symbolName))
                throw new MathException(ErrorCodes.DuplicateVariable, string.Format(Resources.MemberNameAlreadyExists_M, symbolName));


            var domainExpression = expression as DomainExpression ?? new DomainExpression(new[] { expression }, Runtime);

            var function = new Function(
                symbolName,
                this,
                Runtime,
                domainExpression,
                scope
                );

            function.SetParameters(
                parameters.Select(
                    p => new FunctionParameter(
                        p.Name,
                        function,
                        Runtime,
                        p.DefaultValue ?? new ConstantValueExpression(Runtime.Void, Runtime)
                        )
                    )
                );

            if (scope.Accessability == AccessabilityLevels.Global && !this.Equals(Runtime.RootDomain))
                Runtime.RootDomain.Symbols.Add(symbolName, function);

            Symbols.Add(symbolName, function);

            return function;
        }

        public Function CloneFunction(string newFunctionName, Function sourceFunction, Scope scope = default(Scope))
        {
            if (HasSymbol(newFunctionName))
                throw new MathException(ErrorCodes.DuplicateVariable, string.Format(Resources.MemberNameAlreadyExists_M, newFunctionName));

            var visitor = new ExpressionCloningVisitor(Runtime);
            var expression = (DomainExpression)visitor.Visit(sourceFunction.Value);

            var function = new Function(
                sourceFunction.Name,
                this,
                Runtime,
                expression,
                scope
                );

            function.SetParameters(sourceFunction.Parameters);

            if (scope.Accessability == AccessabilityLevels.Global && !this.Equals(Runtime.RootDomain))
                Runtime.RootDomain.Symbols.Add(newFunctionName, function);

            Symbols.Add(newFunctionName, function);


            return function;
        }

        public MathValue InvokeMember(string memberName, IEnumerable<MathValue> parameters)
        {
            var currentSymbol = GetSymbol(memberName);
            if (currentSymbol == null)
                throw new MathException(ErrorCodes.MemberNotFound, string.Format(Resources.MemberNotFound_M, memberName));

            // Check to make sure we can invoke the symbol
            var callable = currentSymbol as Function;
            if (callable == null)
                throw new MathException(ErrorCodes.ExpectedFunctionOrDomain,
                                        string.Format(Resources.ExpectedFunctionOrDomain, memberName,
                                                      currentSymbol.GetType().Name));

            return callable.Invoke(parameters);
        }

        #endregion

        #region Variable Methods

        public IEnumerable<Variable> LocalVariables
        {
            get { return LocalSymbols.Where(d => d is Variable).Select(d => (Variable)d); }
        }

        public IEnumerable<Variable> AllVariables
        {
            get { return AllSymbols.Where(d => d is Variable).Select(d => (Variable)d); }
        }

        protected Dictionary<string, MathSymbol> Symbols
        {
            get { return _symbols; }
        }

        public Variable GetVariable(string name)
        {
            return GetSymbol(name) as Variable;
        }

        public bool HasVariable(string name)
        {
            return HasSymbol(name);
        }

        public Variable CreateVariable(string symbolName, ValueExpression expression = null, Scope scope = default(Scope))
        {
            if (HasSymbol(symbolName))
                throw new MathException(ErrorCodes.DuplicateVariable, string.Format(Resources.MemberNameAlreadyExists_M, symbolName));

            var variable = new Variable(symbolName, this, Runtime, scope);
            if (expression != null)
                variable.SetValue(expression);

            if (scope.Accessability == AccessabilityLevels.Global && !this.Equals(Runtime.RootDomain))
                Runtime.RootDomain.Symbols.Add(symbolName, variable);

            Symbols.Add(symbolName, variable);

            return variable;
        }

        public Variable CloneVariable(string newName, Variable sourceVariable, Scope scope = default(Scope))
        {
            if (HasSymbol(newName))
                throw new MathException(ErrorCodes.DuplicateVariable, string.Format(Resources.MemberNameAlreadyExists_M, newName));

            var visitor = new ExpressionCloningVisitor(Runtime);
            var expression = (ValueExpression)visitor.Visit(sourceVariable.Value);

            return CreateVariable(newName, expression, scope);
        }

        public Variable SetVariable(string variableName, ValueExpression expression)
        {
            var symbol = GetSymbol(variableName);

            // check to make sure that the symbol exists, otherwise there is nothing to
            // set (should have used SetOrDefineSymbol instead)
            if (symbol == null)
                throw new MathException(ErrorCodes.MemberNotFound, string.Format(Resources.MemberNotFound_M, variableName));

            // symbol already exists, so it must be an assignment.  Only variables
            // may be assigned a value after they are already created, so we know that
            // this must also be a variable, or else an error has occured.
            var variable = symbol as Variable;
            if (variable == null)
                throw new MathException(ErrorCodes.ExpectedVariable, string.Format(Resources.ExpectedVariable_VT, variableName, symbol.GetType().Name));

            // Check our access modifiers...
            if (variableName.Contains('.'))
                if (variable.Scope.Modifier == AccessabilityModifiers.ReadOnly)
                    throw new MathException(ErrorCodes.CannotModifyReadonlyVariable,
                                            "Cannot modify readonly variable outside of it's declaring scope");

            // set the value if the variable allows it
            variable.SetValue(expression);

            return variable;
        }

        public Variable SafeSetVariable(string variableName, ValueExpression expression)
        {
            var variable = GetOrDefineVariable(variableName);

            // Check our access modifiers...
            if (variableName.Contains('.'))
                if (variable.Scope.Modifier == AccessabilityModifiers.ReadOnly)
                    throw new MathException(ErrorCodes.CannotModifyReadonlyVariable,
                                            "Cannot modify readonly variable outside of it's declaring scope");

            // set the value if the variable allows it
            variable.SetValue(expression);

            return variable;
        }

        public Variable GetOrDefineVariable(string symbolName)
        {
            // Determine if the symbol already exists
            var currentSymbol = GetVariable(symbolName);
            if (currentSymbol != null)
                return currentSymbol;

            // if the symbol doesn't exist, check to see if it is a dotted expression
            if (symbolName.Contains('.'))
                throw new MathException(ErrorCodes.CannotDefineImplicitVariable, string.Format(Resources.CannotDefineImplicitVariable_V, symbolName));

            return CreateVariable(symbolName);
        }

        #endregion

        #endregion

        /// <summary>
        /// Returns the string representation of a domain
        /// </summary>
        /// <returns>The Name property of the domain</returns>
        public override string ToString()
        {
            return Name;
        }

        #region Dynamic Members

        /// <summary>
        /// Overrides the dynamic TryInvokeMember method.  Used to dynamically look up functions from .NET code
        /// </summary>
        /// <param name="binder"></param>
        /// <param name="args"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            var memberName = binder.Name;

            var currentSymbol = GetSymbol(memberName);
            if (currentSymbol == null)
                throw new MathException(ErrorCodes.MemberNotFound, string.Format(Resources.MemberNotFound_M, memberName));

            // Check to make sure we can invoke the symbol
            var callable = currentSymbol as Function;
            if (callable == null)
                throw new MathException(ErrorCodes.ExpectedFunctionOrDomain,
                                        string.Format(Resources.ExpectedFunctionOrDomain, memberName,
                                                      currentSymbol.GetType().Name));

            var parameters = args.Select(a => (MathValue)a);
            result = callable.Invoke(parameters);

            return true;
        }

        /// <summary>
        /// Overrides the dynamic TryGetMember method.  Used to dynamically look up variables from .NET code
        /// </summary>
        /// <param name="binder"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            var variable = GetSymbol(binder.Name);
            result = variable.Value;

            return true;
        }

        /// <summary>
        /// Overrides the dynamic TrySetMember method.  Used to dynamically look up variables from .NET code.
        /// </summary>
        /// <param name="binder"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            SetVariable(binder.Name, (ValueExpression)value);
            return true;
        }

        #endregion

        #region Helper Methods

        private void EnterScope()
        {
            _sourceScope = Runtime.CurrentDomain;
            _sourceScope.IsCurrent = false;
            this.IsCurrent = true;

            Runtime.SetCurrentScope(this);

            if (EnteredScope != null)
                EnteredScope(this, EventArgs.Empty);
        }

        #endregion
    }
}
